module PrismBreak {

    export class RegionManager {

        private regionList: Region[];
        private _regionIdTag = "data-region-name";
        private _regionTypeTag = "data-region-type"
        private root: HTMLElement;

        private frameworkApp: PbApp;
        private ConfigManager: ConfigManager;

        public discoveredRegions: Region[] = [];

        constructor(frameworkApp: PbApp, configManager: ConfigManager, el: HTMLElement) {
            this.frameworkApp = frameworkApp;
            this.ConfigManager = configManager;

            this.root = el;

            //Add starting region ('ApplicationRegion, root')
            var regObj = new Region(this.frameworkApp, $(this.root).attr(this._regionIdTag), this.root);

            //start discovering regions
            this.discoveredRegions.push(regObj);
        }

        public LoadRegions(configCollection: ConfigItem[]) {

            var stop = false, parentRoot: HTMLElement;

            //shallow clone
            var jsonModules = configCollection.slice(0);
            var index = 0;
            var region: Region
            var elToInjectHTML: HTMLElement;
            var view: View;

            while (!stop && jsonModules.length) {

                //this is called when we are out of loadable regions.
                //the second time through we are going to investigate further the recently
                //discovered regions
                var loadableModuleRegions: ConfigItem[] = Enumerable.From(jsonModules)
                    .SelectMany((<any>$))
                    .Where((item: ConfigItem) => {
                        return this.RegionExistsInDOM(item.RegionId, this.GetParentFromRegionId(item.RegionId));
                    }).ToArray();

                //EXIT condition
                if (!loadableModuleRegions.length) {
                    stop = true;
                }

                //regions yet to load
                else {

                    //update the json modules collection.
                    //We are going to load the 'loadable regions'                    
                    jsonModules = this.DoDiffArray(jsonModules, loadableModuleRegions);

                    while (loadableModuleRegions.length) {
                        var moduleRegion = loadableModuleRegions.pop();

                        //Get region (already discovered) with such id
                        region = this.GetRegionById(moduleRegion.RegionId);

                        //find region in DOM
                        parentRoot = this.GetRegionHTMLById(moduleRegion.RegionId);

                        //load markup to inject in above region
                        elToInjectHTML = this.LoadMarkup(moduleRegion.Markup, false);

                        //If we have a view to load, enrich html with unique view id 
                        if (moduleRegion.ViewId) {
                            elToInjectHTML = $("<div class='view " + moduleRegion.ViewId + "'></div>").append(elToInjectHTML).get(0);
                            //elToInjectHTML = $(elToInjectHTML).wrap("<div class='view " + jsonModule.ViewId + "'></div>").parent().get(0);
                        }

                        //Apply injection!
                        this.InjectMarkup(elToInjectHTML, parentRoot);
                        this.AttachView(elToInjectHTML, region, moduleRegion);

                        // Recursive look for new regions in markup just loaded 
                        this.DiscoverNewRegions(elToInjectHTML);
                    }
                }
            }
        }

        private AttachView(el: HTMLElement, region: PrismBreak.Region, moduleRegion: PrismBreak.ConfigItem): void {

            var view: View;

            //initialize viewmodel. This will start data-binding
            var viewModel = this.ConfigManager.ViewModels[moduleRegion.ViewModelType];

            //create a view instance
            view = new View(this.frameworkApp, moduleRegion.ViewId, viewModel, el, region);


            this.frameworkApp.ViewManager.AddView(view);
            viewModel.Initialize(view);

            if (moduleRegion.Visible) {
                this.frameworkApp.ViewManager.ActivateView(view);
            }

            else {
                //By default the view is invisible
                $(view.Node).addClass(PrismBreak.ViewClass[PrismBreak.ViewClass.pb_invisible]);
            }


        }

        //#regionMarkup
        private LoadMarkup(path: string, isAsync: boolean): HTMLElement {

            var html: HTMLElement;

            $.ajax({
                async: isAsync,
                global: false,
                url: path,
                dataType: "html",
                success: (data: HTMLElement) => {

                    if (!isAsync) {
                        //html = $(data).get(0);
                        html = (data);

                    }
                    else {
                        //TODO: invoke callback

                    }
                },
                error: () => {
                    console.log("error");
                }
            });

            return html;

        }

        private InjectMarkup(elToInject: HTMLElement, parentEl: HTMLElement): void {
            var regionType = $(parentEl).attr(this._regionTypeTag);
            $(parentEl).append(elToInject);

            //if (regionType === RegionType[RegionType.Appendable]) {
            //}
        }
        //#endregion

        //#regionRegion helper methods

        public AddRegion(region: any): any[] {

            this.regionList.push(region);

            return this.regionList;
        }

        private DiscoverNewRegions(parent: HTMLElement): void {
            var newRegion: Region, regionType: string;
            var _this = this;

            //http://stackoverflow.com/questions/14645806/get-all-attributes-of-an-element-using-jquery
            $(parent).each(function () {
                if (this.attributes) {
                    $.each(this.attributes, function (htmlIndex: number, htmlVal: any) {
                        // this.attributes is not a plain object, but an array
                        // of attribute nodes, which contain both the name and value
                        if (this.specified) {
                            if (this.name === _this._regionIdTag) {

                                //instantiate new region
                                newRegion = new Region(_this.frameworkApp, this.value, htmlVal.ownerElement);
                                regionType = $(htmlVal.ownerElement.outerHTML).attr(_this._regionTypeTag);

                                //has type, assign type
                                if (regionType) {
                                    newRegion.Type = RegionType[regionType];
                                }

                                _this.discoveredRegions.push(newRegion);
                            }
                        }
                    });
                }

                //recursion
                $(this).children().each(function () {
                    _this.DiscoverNewRegions(this);
                });
            });
        }

        private RegionExistsInDOM(regionId: string, el: HTMLElement): boolean {

            var _regionId = $(el).attr(this._regionIdTag);

            if (!_regionId || !_regionId.length) {
                _regionId = $(el).children().attr(this._regionIdTag);
            }

            return _regionId === regionId;

        }

        private GetRegionHTMLById(id: string, el?: HTMLElement): HTMLElement {

            var parent = el;
            if (!parent) {
                parent = this.root;
            }

            var region = $("[" + this._regionIdTag + "='" + id + "'", el);

            if (region.length > 1) {
                throw new Error("RegionId not unique");
            }
            else if (!region.length) {
                throw new Error("Region not found. Id queried: " + id);
            }
            else {
                return region[0];
            }

        }

        private GetRegionById(id: string): Region {

            var region: Region = Enumerable.From(this.discoveredRegions)
                .Where((item: Region) => {
                    return item.Id === id;
                }).SingleOrDefault(null);

            if (region) {
                return region;
            }

            else {
                return null;
            }
        }

        private GetParentFromRegionId(id: string): HTMLElement {

            var region = this.GetRegionById(id);

            if (region) {
                return region.Root;
            }

            else {
                return null;
            }
        }
        //#endregion

        //#regionGeneric helper methods
        private DoDiffArray(a: ConfigItem[], b: ConfigItem[]): ConfigItem[] {
            //http://jsperf.com/difference-between-two-arrays-with-native-javascript-or
            var baz: ConfigItem[] = [];

            a.forEach(function (key) {
                if (-1 === b.indexOf(key)) {
                    baz.push(key);
                }
            }, this);

            return baz;
        }
        //#endregion

    }

    export enum RegionType {
        OnlyOne,
        Appendable

    }

    export enum ViewClass {
        pb_visible,
        pb_invisible
    }

} 